<?php

/**
 * @copyright Nichlas Severinsen
 * @license   https://opensource.org/licenses/BSD-2-Clause
 */

namespace Necklace\XxHash;

use Exception;
use FFI;

final class XxHash64 extends XxHash
{
	/**
	 * Constructor.
	 */
	public function __construct()
    {
		parent::__construct();

		$this->ffi = FFI::load(__DIR__ . '/../include/xxhash64-php.h');

		$this->state = $this->ffi->XXH64_createState();
	}

	/**
	 * Destructor.
	 */
	public function __destruct()
	{
		if($this->ffi->XXH64_freeState($this->state) !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to free state');
		}
	}

	/**
	 * Similar to the native XXH64, but returns the hash as a string instead.
	 *
	 * Notice: the xxhash documentation recommends using this function for small inputs only.
	 *
	 * @param  string $data   Data you want to hash.
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return string
	 */
	public function hash(string $data, int $seed = null, string $secret = null): string
	{
		if($secret !== null)
		{
			throw new Exception('XxHash unable to reset with secret. Only supported in XXH3.');
		}

		$hash = $this->ffi->XXH64($data, strlen($data), $seed);

		$canonical = $this->ffi->new('XXH64_canonical_t');

		$this->ffi->XXH64_canonicalFromHash(FFI::addr($canonical), $hash);

		return $this->canonicalToString($canonical);
	}

	/**
	 * Starts a new hash by initializing state (with or without seed).
	 *
	 * @param  int    $seed   Seed used to alter the result.
	 * @param  string $secret Secret used to alter the result.
	 * @return bool
	 */
	public function reset(int $seed = null, string $secret = null): bool
	{
		if($secret !== null)
		{
			throw new Exception('XxHash unable to reset with secret. Only supported in XXH3.');
		}

		if($this->ffi->XXH64_reset($this->state, $seed) !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to reset state');
		}

		return true;
	}

	/**
	 * Feed the hash state with data.
	 *
	 * @param  string $data Feeding data.
	 * @return bool
	 */
	public function update(string $data): bool
	{
		if($this->ffi->XXH64_update($this->state, $data, strlen($data)) !== XxHashErrorCode::OK)
		{
			throw new Exception('XxHash unable to update state');
		}

		return true;
	}

	/**
	 * Produce a hash from the fed data.
	 *
	 * @return string
	 */
	public function digest(): string
	{
		$hash = $this->ffi->XXH64_digest($this->state);

		$canonical = $this->ffi->new('XXH64_canonical_t');

		$this->ffi->XXH64_canonicalFromHash(FFI::addr($canonical), $hash);

		return $this->canonicalToString($canonical);
	}
}
